iT邦幫忙

2025 iThome 鐵人賽

DAY 23
0
Mobile Development

《30 天 Flutter:跨平台 AI 行程規劃 App》系列 第 23

Day 23 - 地圖實戰:把每天行程串成路線圖,一鍵就出發

  • 分享至 

  • xImage
  •  

上一篇入門地圖套件的各參數並可以簡易使用,今天就要把行程跟地圖真正串起來,讓使用者可以直覺地看到每天的活動分布。

這次的目標是:

  • 新增地圖頁面將當天所有行程用 Marker 顯示,並用 Polyline 把活動串起來,清楚呈現路線。
  • 點擊地點可以直接開啟手機內建地圖導航,方便使用者實際出發。

簡單說,就是要把「一堆行程資料」變成「活生生的地圖行程」,讓使用者一眼就能掌握每天的路線。這篇我會一路記錄我怎麼做,碰到什麼坑,怎麼解。

資料庫添加欄位

經緯度為 double ,在 SQLite(也是 Drift 底層使用的資料庫)裡,REAL 是一個 浮點數(floating point number)型態。

class ActivitiesTable extends Table {
  // 新增經緯度欄位
  RealColumn get longitude => real().nullable()();
  RealColumn get latitude => real().nullable()();
}

地圖頁面實作

標記每日行程與串接路線

在地圖上,我們希望同時 標出每日行程的景點,並用 Polyline 將它們串成一條完整路線。以下程式碼示範如何達成:

Scaffold(
  appBar: AppBar(title: const Text("行程地圖")),
  body: asyncLatLngs.when(
    data: (latLngs) {
      if (latLngs.isEmpty) return const Center(child: Text("解析失敗"));

      // 建立 Marker 集合
      final markers = latLngs.asMap().entries.map((entry) {
        final idx = entry.key;
        final latLng = entry.value;
        return Marker(
          markerId: MarkerId("marker_$idx"),
          position: latLng,
          infoWindow: InfoWindow(title: "景點 ${idx + 1}"),
        );
      }).toSet();

      // 建立 Polyline 將景點連線
      final polyline = Polyline(
        polylineId: const PolylineId("route"),
        points: latLngs,
        color: Colors.blue,
        width: 4,
      );

      return GoogleMap(
        initialCameraPosition: CameraPosition(
          target: latLngs.first, // 預設鏡頭定位到第一個景點
          zoom: 14,
        ),
        markers: markers,        // 加入所有 Marker
        polylines: {polyline},   // 加入行程路線
      );
    },
    loading: () => const Center(child: CircularProgressIndicator()),
    error: (err, stack) => Center(child: Text("錯誤: $err")),
  ),
);

顯示全行程地圖

在 Google Maps 中,如果只依靠 CameraPositionzoom 參數來設定顯示範圍,往往只能固定在某一個縮放等級。若想要自動縮放並同時涵蓋所有的標記(Markers),就可以利用 LatLngBounds 搭配 CameraUpdate.newLatLngBounds 來達成所謂的 bounds fitting

以下範例示範如何計算所有座標點的邊界(最南、最北、最西、最東),並讓地圖鏡頭自動移動與縮放到剛好包含全部標記的位置:

GoogleMap(
  onMapCreated: (controller) async {
    _mapController = controller;
    
    // 建立 LatLngBounds,計算出最南、最北、最西、最東的經緯度
    if (latLngs.isNotEmpty) {
      double south = latLngs.first.latitude;
      double north = latLngs.first.latitude;
      double west = latLngs.first.longitude;
      double east = latLngs.first.longitude;

      for (var latLng in latLngs) {
        if (latLng.latitude > north) north = latLng.latitude;
        if (latLng.latitude < south) south = latLng.latitude;
        if (latLng.longitude > east) east = latLng.longitude;
        if (latLng.longitude < west) west = latLng.longitude;
      }

      final bounds = LatLngBounds(
        southwest: LatLng(south, west),
        northeast: LatLng(north, east),
      );

      // 更新地圖鏡頭,padding 可用來保留邊界空間
      _mapController.animateCamera(
        CameraUpdate.newLatLngBounds(bounds, 50), // padding 50
      );
    }
  },
  markers: markers,
  polylines: {polyline},
)

這樣一來,不論行程中有多少個地點,地圖都會自動縮放到最佳比例,讓使用者一眼就能看到完整路線。

點擊導航:直接打開手機內建地圖

要讓 App 點擊 Marker 後打開原生地圖,需要先安裝 url_launcher 套件,並在 iOS 專案ios/Runner/Info.plist 中加入允許的 URL Scheme:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>maps</string>
    <string>comgooglemaps</string>
</array>

接著在建立 Marker 時,設定 onTap 事件呼叫開啟地圖的函式:

Marker(
  markerId: MarkerId("marker_$idx"),
  position: latLng,
  infoWindow: InfoWindow(title: "景點 ${idx + 1}"),
  onTap: () {
    openMapsApp(latLng);
  },
);

openMapsApp 函式可以使用 url_launcher 打開 Apple Maps 或 Google Maps,例如:

Future<void> openMapsApp(LatLng destination) async {
  final googleMapsScheme = Uri.parse(
    'comgooglemaps://?daddr=${destination.latitude},${destination.longitude}',
  );
  final googleMapsWeb = Uri.parse(
    'https://www.google.com/maps/dir/?api=1&destination=${destination.latitude},${destination.longitude}',
  );
  final appleMapsScheme = Uri.parse(
    'maps://?daddr=${destination.latitude},${destination.longitude}&dirflg=d',
  );
  try {
    // 先檢查 Google Maps app
    if (await canLaunchUrl(googleMapsScheme)) {
      await launchUrl(googleMapsScheme);
      return;
    }

    // iOS 原生地圖
    if (await canLaunchUrl(appleMapsScheme)) {
      await launchUrl(appleMapsScheme);
      return;
    }

    // fallback 網頁
    await launchUrl(googleMapsWeb, mode: LaunchMode.externalApplication);
  } catch (e) {
    // ignore
  }
}

這樣使用者點擊 Marker 就能直接跳到原生地圖導航。

今日成果

Android iOS

上一篇
Day 22 - 行程 APP 沒地圖怎麼行?從零打造地圖互動功能,試試 ChatGPT 的教學體驗
下一篇
Day 24 - 地圖實戰:客製地圖 Marker 讓行程一看就懂
系列文
《30 天 Flutter:跨平台 AI 行程規劃 App》24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言